SuperStrict

Rem
bbdoc: cfg file module
about: Use this module to work with .ini/.cfg style configuration files.
This module stores the config file in local memory, and provides functions to
read from and write to this data in local memory, as well as read and write
the data to and from the hard drive. Blank lines, whitespace and comments are 
all supported.

Configuration data is thought of as existing in key/value pairs in the format of:
&{
key=value
}

For example, look at this sample configuration file:
&{
app_width=640
app_height=480
highscore=1000000
;This is a comment!
fullscreen=1
title=My Custom Application ;comment is ignored
}

This module also has the unique feature of having defaults. If a key in the config file
does not have a value, or the key/value pair is missing all together, a default value is
automatically used!
End Rem
Module jmd.cfg

ModuleInfo "Version: 1.2"
ModuleInfo "Author: Jonathan Decker"
ModuleInfo "License: GNU GPL v3"
ModuleInfo "SVN: http://jmd-blitzmax-modules.googlecode.com/svn/trunk/jmd.mod/cfg.mod"

ModuleInfo "History: 1.2"
ModuleInfo "History:	Added ConfigRemove() function / Remove() method"
ModuleInfo "History: 1.1"
ModuleInfo "History: 	Changed how history is annotated"
ModuleInfo "History:	Changed SVN information"
ModuleInfo "History: 1.0"
ModuleInfo "History:	Initial release."

Import brl.map
Import brl.stream
Import brl.retro

Rem
bbdoc: TConfig constant
about: Returned from #ConfigUpdate() if the key/value pair is not present in the config file and the @autoCreate parameter is false.
End Rem
Const CFG_NOVALUE:Int 		= 0
Rem
bbdoc: TConfig constant
about: Returned from #ConfigUpdate() if the key/value pair was successfully updated
End Rem
Const CFG_VALUE_UPDATED:Int = 1
Rem
bbdoc: TConfig constant
about: Returned from #ConfigUpdate() if the key/value pair is not present in the config file and the @autoCreate parameter is True.
(This effectively means that the key/value pair was added if it didn't already exist)
End Rem
Const CFG_VALUE_CREATED:Int = 2

Rem
bbdoc: TConfig type
End Rem
Type TConfig
	Field filePath:String
	Field lines:String[]
	Field defaults:TMap
	
	Rem
	bbdoc: alternative for #CreateConfig()
	End Rem
	Function Create:TConfig(thisPath:String,autoRead:Int=False)
		Local a:TConfig
		a = New TConfig
		a.defaults=CreateMap()
		a.filePath=thisPath
		If autoRead Then a.Read()
		Return a
	End Function
	
		
	'---------------------
	'File Related Methods!
	'---------------------
	Rem
	bbdoc: alternative for #ConfigRead()
	End Rem
	Method Read()
		Local fs:TStream
		Local fileString:String
		
		fs=ReadStream(Self.filePath)
		If fs 
			While Not Eof(fs)
				fileString = fileString + ReadLine(fs) + "~n"
			Wend
			lines=fileString.Split("~n")
			CloseStream(fs)	
			'Lines[] now has 1 extra element due to splitting on ~n
			'Lets make it right!
			Local tempArray:String[Self.lines.length-1]
			Local i:Int
			
			For i = 0 To tempArray.length-1
				tempArray[i]=lines[i]
			Next
			lines = New String[tempArray.length]
			lines = tempArray
		EndIf
		'Print "Total Lines: " + lines.length
			
	End Method
	
	Rem
	bbdoc: alternative for #ConfigWrite()
	End Rem
	Method Write()
		Local fs:TStream
		Local i:Int
		
		fs=WriteStream(Self.filePath)
		If Not fs Then RuntimeError("Could not find file: " + Self.filePath)
		For i=0 To lines.length-1
			WriteLine(fs,lines[i])
		Next
		CloseStream(fs)
	End Method
	
	'------------------------
	'Default Related Methods!
	'------------------------
	Rem
	bbdoc: alternative for #ConfigSetDefault()
	End Rem
	Method SetDefault(thisKey:String,value:String)
		If MapContains(defaults,thisKey)
			defaults.remove(thisKey)
		EndIf
		defaults.insert(thisKey,value)
	End Method
	
	Rem
	bbdoc: alternative for #ConfigSetDefaults()
	End Rem
	Method SetDefaults(thisMap:TMap)
		Self.defaults=thisMap
	End Method
	
	
	'==================
	'ACCESSOR FUNCTIONS
	'==================
	'For internal use
	Method Get:String(thisKey:String)
		Local i:Int
		Local s:String
		Local ret:Int
		Local token:String
		Local value:String
		Local thisLine:String
		
		
		For i=0 To lines.length-1
			thisLine=lines[i]
			
			'Remove leading/trailing spaces
			thisLine=thisLine.Trim()

			'Remove comments
			ret=thisLine.find(";") 'TODO: support escape sequence! for ";" char!
			If ret<>-1
				thisLine=Mid(thisLine,0,ret)
			End If

			'skip blank lines
			If thisLine.Trim()=""
				Continue
			EndIf
			'find the token
			If Mid(thisLine,0,thisKey.length+1)=thisKey
				'POSSIBLY found it!
				thisLine=Mid(thisLine,thisKey.length+1,-1)
				thisLine=thisLine.Trim()
				'Make sure this isn't just a longer variable name!
				
				If Chr(thisLine[0])=" " Or Chr(thisLine[0])="=" Or Chr(thisLine[0])="~t"
					'Is there an = sign?
					If thisLine.length=0 
						Return ""
					EndIf
					If Chr(thisLine[0])<>"="
						Return ""
					Else
						thisLine=Mid(thisLine,2,-1)
						thisLine=thisLine.Trim()
	
						Return thisLine
					EndIf
				EndIf
				
			Else
				Continue
			EndIf
		Next
	End Method
	
	Rem
	bbdoc: alternative for #ConfigGetString()
	End Rem
	Method GetString:String(thisKey:String)
		Local s:String
		
		s=Get(thisKey)
		If s=""
			'use default
			Return String(defaults.valueforkey(thisKey))
		Else
			Return String(s)
		EndIf
	End Method
	
	Rem
	bbdoc: alternative for #ConfigGetInt()
	End Rem
	Method GetInt:Int(thisKey:String)
		Local s:String
		
		s=Get(thisKey)
		If s=""
			'use default
			Return Int(String(defaults.valueforkey(thisKey)))
		Else
			Return Int(s)
		EndIf
	End Method
	
	Rem
	bbdoc: alternative for #ConfigGetFloat()
	End Rem
	Method GetFloat:Float(thisKey:String)
		Local s:String
		
		s=Get(thisKey)
		If s=""
			'use default
			Return Float(String(defaults.valueforkey(thisKey)))
		Else
			Return Float(s)
		EndIf
	End Method
	
	
	'TODO - parameters to tell whether to return full path, relative path, etc..
	Rem
	bbdoc: alternative for #ConfigGetOptionPath()
	End Rem
	Method GetOptionPath:String(thisKey:String)
		'if it has no path, look in the same folder as the cfg file
		Local thisFile:String = Self.GetString(thisKey)
		If ExtractDir(thisFile)=""
			Return ExtractDir(Self.filePath) + "/" + thisFile
		Else
			Return thisFile
		EndIf
	End Method
	
	Rem
	bbdoc: alternative for #ConfigGetDefault()
	End Rem
	Method GetDefault:String(thisKey:String)
		If defaults.contains(thisKey)
			Return String(defaults.valueforkey(thisKey))
		Else
			Return ""
		EndIf
	End Method

	Rem
	bbdoc: alternative for #ConfigContains()
	End Rem
	Method Contains:Int(thisKey:String)
	Local i:Int
		Local s:String
		Local thisLine:String
		Local comment:String
		Local ret:Int
		
		For i = 0 To lines.length-1 
			thisLine=lines[i]
			
			'Remove leading/trailing spaces
			thisLine=thisLine.Trim()

			'skip blank lines
			If thisLine.Trim()=""
				Continue
			EndIf
			
			'find the token
			If Mid(thisLine,0,thisKey.length+1)=thisKey
				Return True
			Else
				Continue
			EndIf
		Next
		Return False
	End Method

	
	Rem
	bbdoc: alternative for #ConfigUpdate()
	End Rem
	Method Update:Int(thisKey:String,value:String,autoCreate:Int=True)
		
		Local i:Int
		Local s:String
		Local thisLine:String
		Local comment:String
		Local ret:Int
		
		For i = 0 To lines.length-1
			thisLine=lines[i]
			
			'Remove leading/trailing spaces
			thisLine=thisLine.Trim()

			'Remove comments
			ret=thisLine.find(";") 'TODO: support escape sequence! for ";" char!
			If ret<>-1
				comment=Mid(thisLine,ret+1,-1)
				thisLine=Mid(thisLine,0,ret)
			End If

			'skip blank lines
			If thisLine.Trim()=""
				Continue
			EndIf
			'find the token
			If Mid(thisLine,0,thisKey.length+1)=thisKey
				lines[i]=thisKey + "=" + value + " " + comment
				Return CFG_VALUE_UPDATED
			Else
				Continue
			EndIf
		Next
		'This key-value pair does not exist!
		'Tack on this key-value pair if the autoCreate option is set!
		If autoCreate
			Self.add(thisKey,value) 
			Return CFG_VALUE_CREATED
		Else
			Return CFG_NOVALUE
		EndIf
	End Method
	
	Rem
	bbdoc: alternative for #ConfigAdd()
	End Rem
	Method Add(thisKey:String,value:String)
		Local i:Int
		Local tempArray:String[Self.lines.length+1]

		
		For i=0 To lines.length-1
			tempArray[i]=lines[i]
		Next

		tempArray[i]=thisKey + "=" + value
		
		lines=New String[tempArray.length]
		lines=tempArray
	End Method
	
	Rem
	bbdoc: alternative for #ConfigRemove()
	End Rem
	Method Remove(thisKey:String)
		Local i:Int
		Local thisLine:String
		Local tempArray:String[lines.length]
		Local removed:Int=False
		Local ret:Int 
		Local comment:String
		
		For i=0 To lines.length-1
			thisLine=lines[i]
			
			'Remove leading/trailing spaces
			thisLine=thisLine.Trim()

			'Remove comments
			ret=thisLine.find(";") 'TODO: support escape sequence! for ";" char!
			If ret<>-1
				comment=Mid(thisLine,ret+1,-1)
				thisLine=Mid(thisLine,0,ret)
			End If

			'skip blank lines
			If thisLine.Trim()=""
				Continue
			EndIf
			'find the token
			If Mid(thisLine,0,thisKey.length+1)=thisKey
				'Found it! - skip ading it!
				removed=True
			Else
				'Not it
				tempArray[i]=lines[i]
				
			EndIf
		Next
		lines=tempArray
		lines=lines[0..lines.length-2]
		
	End Method
	
	Rem
	bbdoc: alternative for #ConfigClear()
	End Rem
	Method Clear()
		Self.filePath=""
		Self.defaults.clear()
		Self.lines = New String[0]
	End Method
End Type


Rem
bbdoc: Create a new TConfig instance
about:
Pass in a path to a config/ini file to the @thisPath parameter.
Optional @autoRead parameter will automatically execute the #ConfigRead() method (defaults to @False).
example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",true)
}

Alternatively you can use the @TConfig.Create() function
End Rem
Function CreateConfig:TConfig(thisPath:String,autoRead:Int=False)
	Return TConfig.Create(thisPath,autoRead)
End Function

Rem
bbdoc: Read in the config file
about: Can be used to read, or re-read a config file from the hard drive

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
}
End Rem
Function ConfigRead(thisConfig:TConfig)
	thisConfig.Read()
End Function

Rem
bbdoc: Write out a cfg file
about:
After local changes are made with the #ConfigUpdate or #ConfigAdd functionss, these changes
must be written out to the file with this function  to save the changes in the actual
config file.

example:
&{
local cfg:TConfig = TConfig.Create("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigUpdate(cfg,"app_width","1024",true)
ConfigWrite(cfg)
}
End Rem
Function ConfigWrite(thisConfig:TConfig)
	thisConfig.Write()
End Function

Rem
bbdoc: Set a %single default value
about:
Defaults are used in cases where either no value was assigned to a key in the config file,
or if the key its self wasn't present in the config file.

example:
&{
local cfg:TConfig = TConfig.Create("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefault(cfg,"app_width","1024")
thisWidth:int=ConfigGetInt(cfg,"app_width")
}

note: Key values are entered as a string, regardless of the actual data type.
#ConfigGetInt(), #ConfigGetFloat() and #ConfigGetString() are used while retreiving
the value to cast it to the correct data type!
End Rem
Function ConfigSetDefault(thisConfig:TConfig,thisKey:String,thisValue:String)
	thisConfig.SetDefault(thisKey,thisValue)
End Function

Rem
bbdoc: Set %multiple default values with a #Tmap
about:
Defaults are used in cases where either no value was assigned to a key in the config file,
or if the key its self wasn't present in the config file.

example:
&{
defaults:TMap=CreateMap()
defaults.insert("app_width","1024")
defaults.insert("app_height","768")
defaults.insert("debug_mode","1")

local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefaults(cfg,defaults) 'pass in our map of default key/value pairs
thisWidth:int=ConfigGetInt("app_width") 'if the app_width key is not present in the ini file,
                                        'the default we set earlier will be returned!
}

note: Key values are entered as a string, regardless of the actual data type.
#ConfigGetInt(), #ConfigGetFloat() and #ConfigGetString() are used while retreiving
the value to cast it to the correct data type!
End Rem
Function ConfigSetDefaults(thisConfig:TConfig,thisMap:TMap)
	thisConfig.SetDefaults(thisMap)
End Function

Rem
bbdoc: Get the specified key's value
about:
Returns the the specified key's value as an int.
If the #ConfigSetDefault() or #ConfigSetDefaults() functionss were used, this function will return the
default if the config file does not contain the key searched for.

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefault(cfg,"rotation_z","358.9")
thisRot:Float=ConfigGetFloat("rotation_z")
}
End Rem
Function ConfigGetFloat:Float(thisConfig:TConfig,thisKey:String)
	Return thisConfig.GetFloat(thisKey)
End Function

Rem
bbdoc: Get the specified key's value
about:
Returns the the specified key's value as an int.
If the #ConfigSetDefault() or #ConfigSetDefaults() functionss were used, this function will return the
default if the config file does not contain the key searched for.

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefault(cfg,"high_score","0")
thisScore:int=ConfigGetInt(cfg,"high_score")
}
End Rem
Function ConfigGetInt:Int(thisConfig:TConfig,thisKey:String)
	Return thisConfig.GetInt(thisKey)
End Function

Rem
bbdoc: Get the specified key's value
about:
Returns the the specified key's value as a string.
If the #ConfigSetDefault() or #ConfigSetDefaults() functionss were used, this functiond will return the
default if the config file does not contain the key searched for.

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefault(cfg,"name","Unknown")
thisperson:string=ConfigGetString(cfg,"name")
}
End Rem
Function ConfigGetString:String(thisConfig:TConfig,thisKey:String)
	Return thisConfig.GetString(thisKey)
End Function

Rem
bbdoc: Get the stored default for the given key
about:
This is a convenienc function which returns the default stored for the given key,
or an empty string otherwise.

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
print ConfigGetDefault(cfg,"person")
ConfigAddDefault(cfg,"person","unknown")
print ConfigGetDefault(cfg,"person")
}
End Rem
Function ConfigGetDefault:String(thisConfig:TConfig,thisKey:String)
	thisConfig.GetDefault(thisKey)
End Function

Rem
bbdoc: See if the config file contains a given key
about:
Return True if the config file contains the key, return False otherwise.
Example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
if not ConfigContains(cfg,"author")
	ConfigAdd(cfg,"author","John Doe")
endif
ConfigWrite(cfg)
}
End Rem
Function ConfigContains:Int(thisConfig:TConfig,thisKey:String)
	Return thisConfig.Contains(thisKey)
End Function

Rem
bbdoc: Returns a path value stored with the given key
about:
This is a convenience function to help construct usful paths from the config file.
If the stored path has no directory information, then it is assumed to reside in the same
directory as the config file and a true path is returned - otherwise, the actual
value is returned.
End Rem
Function ConfigGetOptionPath:String(thisConfig:TConfig,thisKey:String)
	Return thisConfig.GetOptionPath(thisKey)
End Function

Rem
bbdoc: Add a new key/value pair to the end of the config file
about:
This updates the internal representation of the config file. To have changes reflected
in the actual config file, make sure to save the changes with the #ConfigWrite() function!

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigAdd(cfg,"author","John Doe")
ConfigWrite(cfg)
}
End Rem
Function ConfigAdd(thisConfig:TConfig,thisKey:String,thisValue:String)
	thisConfig.Add(thisKey,thisValue)
End Function

Rem
bbdoc: Update the value of the given key
about:
The autoCreate parameter will create the key/value pair if it does not exist.

return values:
[
* @CFG_VALUE_UPDATED | if it updated the value
* @CFG_VALUE_CREATED | if the value had to be created
* @CFG_NOVALUE  | if the value doesn't exist and wasn't created
]

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigAdd(cfg,"author","John Doe")
ConfigUpdate(cfg,"author","Jane Doe")
ConfigWrite(cfg)
}

note: Key values are entered as a string, regardless of the actual data type.
#ConfigGetInt(), #ConfigGetFloat() and #ConfigGetString() are used while retreiving
the value to cast it to the correct data type!
End Rem
Function ConfigUpdate:Int(thisConfig:TConfig,thisKey:String,thisValue:String,autoCreate:Int=False)
	Return thisConfig.Update(thisKey,thisValue,autoCreate)
End Function

Rem
bbdoc: remove a key/value pair
about:
Removes the key/value pair from the config file.
End Rem
Function ConfigRemove(thisConfig:TConfig,thisKey:String)
	thisConfig.Remove(thisKey)
End Function

Rem
bbdoc: Clear/Reset the TConfig instance
about:
Completely clears the TConfig instances internal fields. Note that you will have to
manually set the filepath field if you want to continue using this instance after
calling #ConfigClear()!

example:
&{
local cfg:TConfig = CreateConfig("./settings.ini",false) 'TConfig instance
ConfigRead(cfg)
ConfigSetDefault(cfg,"title","My Application")
appTitle:string=ConfigGetString(cfg,"title")
ConfigClear(cfg)
cfg.filePath="./othersettings.ini"
ConfigRead(cfg)
ConfigSetDefault(cfg,"version","1.0")
myVersion:string=ConfigGetString(cfg,"version")
}
End Rem
Function ConfigClear(thisConfig:TConfig)
	thisConfig.Clear()
End Function